package com.panopset;

import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

import com.panopset.compat.RezIO;
import com.panopset.compat.Strings;
import com.panopset.io.DirectoryProcessor;
import com.panopset.io.FileProcessor;
import com.panopset.io.FileProcessor.Listener;
import com.panopset.io.GlobalReplace;
import com.panopset.io.LockedWriter;
import com.panopset.io.StreamProcessor;

/**
 * Implementation details for Util IO functions.
 *
 * @author Karl Dinwiddie
 *
 */
public final class UtilIO {

    /**
     * Global replace text in a directory.
     *
     * @param path
     *            Top level directory. Subdirectories will also be traversed.
     * @param from
     *            Replace String.
     * @param to
     *            Replacement String.
     * @param extension
     *            File extension, ie: &quot;.java&quot;.
     */
    public static void directoryReplace(final String path, final String from,
            final String to, final String extension) {
        new GlobalReplace(new File(path), from, to, extension).exec();
    }

    /**
     * Do a global replacement on all files in a directory, and its
     * sub-directories.
     *
     * @param directory
     *            Target directory.
     * @param from
     *            From text.
     * @param to
     *            To text.
     */
    public static void globalReplace(final File directory, final String from,
            final String to) {
        new DirectoryProcessor(directory) {

            @Override
            protected void process(final File f) {

                new FileProcessor.Builder().file(f)
                        .listener(new FileProcessor.Listener() {

                            private List<String> v = new ArrayList<String>();

                            @Override
                            public void finish(final FileProcessor processor) {
                                StringBuilder sb = new StringBuilder();
                                boolean firstTime = true;
                                for (String s : v) {
                                    if (firstTime) {
                                        firstTime = false;
                                    } else {
                                        sb.append(Strings.getEol());
                                    }
                                    sb.append(s);
                                }
                                saveStringToFile(sb.toString(), f);
                            }

                            @Override
                            public void process(final FileProcessor processor,
                                    final String[] s) {
                                for (String t : s) {
                                    v.add(t.replace(from, to));
                                }
                            }
                        }).construct().exec();
            }
        }

        .exec();
    }

    /**
     * Delete file.
     *
     * @param path
     *            Path to file to delete.
     */
    public static void delete(final String path) {
        if (path == null) {
            return;
        }
        delete(new File(path));
    }

    /**
     * Delete file.
     *
     * @param path
     *            Path to file to delete.
     */
    public static void delete(final File path) {
        if (path == null) {
            return;
        }
        if (!path.exists()) {
            return;
        }
        if (!path.delete()) {
            Util.dspmsg(Util.x("Failed to delete") + " "
                    + RezIO.getCanonicalPath(path));
        }
    }

    /**
     * Create new file.
     *
     * @param path
     *            Path to new file.
     */
    public static void createNewFile(final String path) {
        if (path == null) {
            return;
        }
        createNewFile(new File(path));
    }

    /**
     * Create new file.
     *
     * @param path
     *            Path to file.
     */
    public static void createNewFile(final File path) {
        if (path == null) {
            return;
        }
        File parent = path.getParentFile();
        if (parent != null && !parent.exists()) {
            mkdirs(parent);
        }
        try {
            if (!path.createNewFile()) {
                Util.dspmsg(Util.x("Failed to create") + " "
                        + RezIO.getCanonicalPath(path));
            }
        } catch (IOException e) {
            Util.log(e);
        }
    }

    /**
     * Make directories for a path.
     *
     * @param path
     *            Path to make directories for.
     */
    public static void mkdirs(final String path) {
        if (path == null) {
            return;
        }
        mkdirs(new File(path));
    }

    /**
     * @param dir
     *            Directory file to check.
     * @return True if dir is not an existing file.
     */
    public static boolean isValidDirectory(final File dir) {
        if (dir == null) {
            return false;
        }
        if (dir.exists() && dir.isFile()) {
            return false;
        }
        return true;
    }

    /**
     * Make directories for a path.
     *
     * @param path
     *            Path to make directories for.
     */
    public static void mkdirs(final File path) {
        if (path == null) {
            return;
        }
        if (path.exists()) {
            return;
        }
        if (!path.mkdirs()) {
            Util.dspmsg(Util.x("Unable to create path to") + " "
                    + RezIO.getCanonicalPath(path));
        }
    }

    /**
     * This method will return true if the files are identical, ignoring the
     * kind of return character used.
     *
     * @param one
     *            First file to compare.
     * @param two
     *            Second file to compare.
     * @return true Only if both files exist and are identical.
     */
    public static boolean filesAreSame(final File one, final File two) {
        final boolean[] rtn = new boolean[1];
        rtn[0] = false;
        new FileProcessor.Builder().file(one).file(two)
                .listener(new FileProcessor.Listener() {

                    @Override
                    public void finish(final FileProcessor processor) {
                        rtn[0] = true;
                        processor.stop();
                    }

                    @Override
                    public void process(final FileProcessor processor,
                            final String[] s) {
                        if (s.length != 2 || s[0] == null || s[1] == null
                                || !s[0].equals(s[1])) {
                            processor.stop();
                            return;
                        }
                    }
                }).construct().exec();
        return rtn[0];
    }

    /**
     * Return contents of a file as a String.
     *
     * @param fileName
     *            File name.
     * @return String full text of file.
     */
    public static String getFileText(final String fileName) {
        return getFileText(new File(fileName));
    }

    /**
     * Return contents of a file as a String.
     *
     * @param fileName
     *            File name.
     * @param textFilter
     *            Text filter.
     * @return String full text of file.
     */
    public static String getFileText(final String fileName,
            final TextFilter textFilter) {
        return getFileText(new File(fileName), textFilter);
    }

    /**
     * Return contents of a file as a String.
     *
     * @param file
     *            File.
     * @param textFilter
     *            Text filter.
     * @return String full text of file.
     */
    public static String getFileText(final File file,
            final TextFilter textFilter) {
        StringWriter rtn = new StringWriter();
        boolean firstTime = true;
        if (!file.exists()) {
            return "";
        }
        for (String s : loadFile(file)) {
            if (firstTime) {
                firstTime = false;
            } else {
                rtn.append(Strings.getEol());
            }
            if (textFilter == null) {
                rtn.append(s);
            } else {
                rtn.append(textFilter.filter(s));
            }
        }
        return rtn.toString();
    }

    /**
     * Override the filter method to provide a way to edit lines as they are
     * read.
     */
    public abstract static class TextFilter {
        /**
         * Apply filter.
         *
         * @param s
         *            String to filter.
         * @return Filtered value.
         */
        public abstract String filter(final String s);
    }

    /**
     * Return contents of a file as a String.
     *
     * @param file
     *            File.
     * @return String full text of file.
     */
    public static String getFileText(final File file) {
        return getFileText(file, null);
    }

    /**
     * Load an object from a file.
     *
     * @param clazz
     *            Class type to load.
     * @param file
     *            File to load from.
     * @return Loaded Object.
     */
    public static Object loadObject(final Class<?> clazz, final File file) {
        Object rtn = null;
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        if (file.exists()) {
            try {
                fis = new FileInputStream(file);
                ois = new ObjectInputStream(fis);
                rtn = ois.readObject();
            } catch (ClassNotFoundException ex) {
                // Delete the file if it no longer matches a class.
                delete(file);
            } catch (InvalidClassException ex) {
                Util.log(ex.getMessage());
            } catch (Exception ex) {
                Util.log(Util.x("Failed to load") + ": "
                        + RezIO.getCanonicalPath(file));
                Util.log(ex);
            } finally {
                try {
                    if (ois != null) {
                        ois.close();
                    }
                    if (fis != null) {
                        fis.close();
                    }
                } catch (Exception ex) {
                    Util.log(ex);
                }
            }
        }
        if (rtn == null) {
            try {
                rtn = clazz.newInstance();
            } catch (Throwable t) {
                Util.log(t);
                return null;
            }
        }
        return rtn;
    }

    /**
     * Save Object to file.
     *
     * @param object
     *            Object to save.
     * @param file
     *            File to save Object to.
     * @return true iff successful
     */
    public static boolean saveObject(final Object object, final File file) {
        if (!file.exists()) {
            File p = file.getParentFile();
            mkdirs(p);
        }
        try {
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(object);
            oos.flush();
            oos.close();
            fos.close();
            return true;
        } catch (Exception ex) {
            Util.log(Util.x("Failed to save") + ": "
                    + RezIO.getCanonicalPath(file));
            ex.printStackTrace();
            return false;
        }
    }

    /**
     * Create parent directories for a file.
     *
     * @param file
     *            File to create parent directories for.
     * @throws IOException
     *             IOException.
     */
    public static void createParentDirectories(final File file)
            throws IOException {
        if (!file.exists()) {
            File parentDir = file.getParentFile();
            if (parentDir == null) {
                // Check to see if the file has a relative path.
                File f2 = new File(RezIO.getCanonicalPath(file));
                parentDir = f2.getParentFile();
                if (parentDir == null) {
                    throw new IOException();
                }
            }
            if (parentDir.exists()) {
                return;
            }
            mkdirs(parentDir);
        }
    }

    /**
     * Copy file.
     *
     * @param from
     *            From file.
     * @param to
     *            To file.
     * @return true iff successful
     */
    public static boolean copyFile(final File from, final File to) {
        try {
            createParentDirectories(to);
            StreamProcessor.copyStream(new FileInputStream(from),
                    new FileOutputStream(to));
            return true;
        } catch (Exception ex) {
            Util.log(Util.x("from") + ": " + RezIO.getCanonicalPath(from));
            Util.log(Util.x("  to") + ": " + RezIO.getCanonicalPath(to));
            Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
    }

    /**
     *
     * @param from
     *            From file name.
     * @param to
     *            To file name.
     * @throws Exception
     *             Exception.
     */
    public static void copyTextFile(final String from, final String to)
            throws Exception {
        copyTextFile(new File(from), new File(to));
    }

    /**
     *
     * @param from
     *            From file.
     * @param to
     *            To file.
     * @throws Exception
     *             Exception.
     */
    public static void copyTextFile(final File from, final File to)
            throws Exception {
        try {
            if (!from.exists()) {
                Util.log(RezIO.getCanonicalPath(to));
                throw new FileNotFoundException();
            }
            createParentDirectories(to);
            BufferedReader br = new BufferedReader(new FileReader(from));
            BufferedWriter bw = new BufferedWriter(new FileWriter(to));
            String s = br.readLine();
            while (s != null) {
                bw.write(s);
                bw.write(Strings.getEol());
                s = br.readLine();
            }
            bw.flush();
            bw.close();
            br.close();
        } catch (Exception ex) {
            Util.log(Util.x("from") + ": " + RezIO.getCanonicalPath(from));
            Util.log(Util.x("  to") + ": " + RezIO.getCanonicalPath(to));
            throw ex;
        }
    }

    /**
     * Copy file.
     *
     * @param from
     *            From file.
     * @param to
     *            To file.
     * @param encoding
     *            Encoding.
     * @return true iff successful
     */
    public static boolean copyFile(final File from, final File to,
            final String encoding) {
        try {
            createParentDirectories(to);
            StreamProcessor.copyStream(new InputStreamReader(
                    new FileInputStream(from), encoding),
                    new OutputStreamWriter(new FileOutputStream(to), encoding));
            return true;
        } catch (Exception ex) {
            Util.log(Util.x("from") + ": " + RezIO.getCanonicalPath(from));
            Util.log(Util.x("  to") + ": " + RezIO.getCanonicalPath(to));
            Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
    }

    /**
     * Save String to file.
     *
     * @param str
     *            String to save.
     * @param append
     *            True if appending to end of file, false if replacing file.
     * @param file
     *            File to save String to.
     * @return true iff successful
     */
    public static boolean saveStringToFile(final String str, final File file,
            final boolean append) {
        try {
            createParentDirectories(file);
            FileWriter fw = new FileWriter(file, append);
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(str);
            bw.close();
            fw.close();
            return true;
        } catch (IOException ex) {
            Util.log(Util.x("file") + ": " + RezIO.getCanonicalPath(file));
            Util.log(ex);
            return false;
        }
    }

    /**
     * Save String to file.
     *
     * @param str
     *            String to save.
     * @param file
     *            File to save String to.
     * @return true iff successful
     */
    public static boolean saveStringToFile(final String str, final File file) {
        return saveStringToFile(str, file, false);
    }

    /**
     * Save bytes to file.
     *
     * @param bytes
     *            Bytes to save.
     * @param file
     *            File to save bytes to.
     * @return true iff successful
     */
    public static boolean saveBytesToFile(final byte[] bytes,

    final File file) {
        try {
            createParentDirectories(file);
            FileOutputStream fw = new FileOutputStream(file);
            BufferedOutputStream bw = new BufferedOutputStream(fw);
            bw.write(bytes);
            bw.close();
            fw.close();
            return true;
        } catch (IOException ex) {
            Util.log(Util.x("file") + ": " + RezIO.getCanonicalPath(file));
            Util.log(ex);
            return false;
        }
    }

    /**
     * Copy library resource.
     *
     * @param resourcePath
     *            Resource path.
     * @param targetFile
     *            Target file.
     * @throws IOException
     *             IO Exception.
     */
    public static void copyLibraryResource(final String resourcePath,
            final File targetFile) throws IOException {
        try {
            createParentDirectories(targetFile);
            BufferedInputStream bis = new BufferedInputStream(
                    Util.class.getResourceAsStream(resourcePath));
            BufferedOutputStream bos = new BufferedOutputStream(
                    new FileOutputStream(targetFile));
            StreamProcessor.copyStream(bis, bos);
        } catch (IOException ex) {
            Util.log(Util.x("resourcePath") + ": " + resourcePath);
            Util.log(Util.x("targetFile") + ": "
                    + RezIO.getCanonicalPath(targetFile));
            Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex);
            throw (ex);
        }
    }

    /**
     * Saves a text file, replacing return characters with standard return
     * characters. Intended for relatively small files.
     *
     * @see com.panopset.compat.Strings#getEol()
     * @param text
     *            String.
     * @param file
     *            File.
     */
    public void saveTextFile(final String text, final File file) {
        try {
            FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw);
            StringReader sr = new StringReader(text);
            BufferedReader br = new BufferedReader(sr);
            String l = br.readLine();
            while (l != null) {
                bw.write(l);
                bw.write(Strings.getEol());
                l = br.readLine();
            }
            bw.flush();
            bw.close();
            fw.close();
            sr.close();
            br.close();
        } catch (IOException e) {
            Util.log(e);
        }
    }

    /**
     * Load a file into an array. This should be done with reasonable sized text
     * files.
     *
     * @param file
     *            File to load.
     * @return array Array to load file in to.
     */
    public static List<String> loadFile(final File file) {
        final List<String> lines = new ArrayList<String>();
        if (file == null) {
            return lines;
        }
        if (!file.exists()) {
            return lines;
        }
        new FileProcessor.Builder()
                .fileAndListener(file, new FileProcessor.Listener() {

                    @Override
                    public void finish(final FileProcessor processor) {
                        // nothing to do
                    }

                    @Override
                    public void process(final FileProcessor processor,
                            final String[] s) {
                        lines.add(s[0]);
                    }
                }).construct().exec();
        return lines;
    }

    /**
     * Save package resource.
     *
     * @param packagePath
     *            Package path, ie: com.panopset.something
     * @param directoryPath
     *            Directory path, ie: mylocaldrive/something
     * @return File that resource was saved to.
     * @throws IOException
     *             IO Exception.
     */
    public static File savePackageResource(final String packagePath,
            final String directoryPath) throws IOException {
        String target = directoryPath + "/" + packagePath;
        File tf = new File(target);
        copyLibraryResource(packagePath, tf);
        return tf;
    }

    /**
     * Save package resource, valid characters only.
     *
     * @param packagePath
     *            Package path, ie: com.panopset.something
     * @param directoryPath
     *            Directory path, ie: mylocaldrive/something
     * @return File that resource was saved to.
     * @throws IOException
     *             IO Exception.
     */
    public static File savePackageResourceCharOnly(final String packagePath,
            final String directoryPath) throws IOException {
        String target = directoryPath + "/" + packagePath;
        File tf = new File(target);
        copyLibraryResource(packagePath, tf);
        return tf;
    }

    /**
     * Get Properties object populated from a package resource file.
     *
     * @param clazz
     *            Class who's package contains the resource.
     * @param name
     *            Name of resource.
     * @return Properties loaded from resource.
     */
    public static Properties loadProperties(final Class<?> clazz,
            final String name) {
        Properties p = new Properties();
        final String path = RezIO.getResourcePath(clazz, name);
        BufferedInputStream bis = getResourceStream(path);
        try {
            p.load(bis);
        } catch (IOException e) {
            com.panopset.Util.log(e);
        }
        return p;
    }

    /**
     * Get resource stream.
     *
     * @param resourcePath
     *            Resource path.
     * @return Buffered input stream.
     */
    public static BufferedInputStream getResourceStream(
            final String resourcePath) {
        return new BufferedInputStream(
                UtilIO.class.getResourceAsStream(resourcePath));
    }

    /**
     * Add if populated.
     *
     * @param v
     *            String List.
     * @param s
     *            String to add if it is populated.
     */
    public static void addIfPopulated(final List<String> v, final String s) {
        if (v == null) {
            return;
        }
        if (s == null) {
            return;
        }
        if (Strings.isPopulated(s)) {
            v.add(s);
        }
    }

    /**
     * ie: changeExtension(new
     * File("/home/app/nls/en.properties"),".properties", ".txt").
     *
     * @param f
     *            File to change extensions in
     * @param oldExtension
     *            Old extension.
     * @param newExtension
     *            New extension.
     */
    public static void changeExtension(final File f, final String oldExtension,
            final String newExtension) {
        if (!Strings.isPopulated(oldExtension)
                || oldExtension.equals(Strings.getExtension(f.getName()))) {
            File n = new File(Strings.dropExtension(RezIO.getCanonicalPath(f))
                    + "/" + newExtension);
            copyFile(f, n);
            delete(f);
        }
    }

    /**
     * ie: changeExtensions(new File("/home/app/nls"),".properties", ".txt").
     *
     * @param directory
     *            Directory to change extensions in
     * @param oldExtension
     *            Only files with this extension will be changed. If blank or
     *            null, all files affected.
     * @param newExtension
     *            Affected files will get this new extension, with the old
     *            extension, if any, dropped.s
     */
    public static void changeExtensions(final File directory,
            final String oldExtension, final String newExtension) {
        for (File f : directory.listFiles()) {
            if (f.isDirectory()) {
                changeExtensions(f, oldExtension, newExtension);
            } else {
                changeExtension(f, oldExtension, newExtension);
            }
        }
    }

    /**
     * Save panel image to a file.
     *
     * @param f
     *            File.
     * @param c
     *            Panel.
     */
    public static void savePanel2file(final File f, final JPanel c) {
        Dimension sz = c.getSize();
        final BufferedImage image = new BufferedImage(sz.width, sz.height,
                BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        c.paint(g);
        saveFinishedImage2file(f, image);
    }

    /**
     * Save canvas to file.
     *
     * @param f
     *            File to save canvas image to.
     * @param c
     *            Canvas to save image from.
     */
    public static void saveCanvas2file(final File f, final Canvas c) {
        Dimension sz = c.getSize();
        final BufferedImage image = new BufferedImage(sz.width, sz.height,
                BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        c.paint(g);
        saveFinishedImage2file(f, image);
    }

    /**
     * Save image to file.
     *
     * @param f
     *            File to save image to.
     * @param i
     *            Image to save to file.
     */
    public static void saveImage2file(final File f, final Image i) {
        final BufferedImage image = new BufferedImage(i.getWidth(null),
                i.getHeight(null), BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        g.drawImage(i, 0, 0, null);
        saveFinishedImage2file(f, image);
    }

    /**
     * Save finished image to file.
     *
     * @param f
     *            File to save image to.
     * @param bi
     *            Finished buffered image to save from.
     */
    public static void saveFinishedImage2file(final File f,
            final BufferedImage bi) {
        String path = f.getPath();
        String fileType = path.substring(path.lastIndexOf(".") + 1);
        assert fileType != null && fileType.length() > 0;
        try {
            ImageIO.write(bi, fileType, f);
        } catch (IOException e) {
            Util.dspmsg("Check permissions to " + RezIO.getCanonicalPath(f));
            Util.log(e);
        }
    }

    /**
     * Extract zip file.
     *
     * @param dir
     *            Directory to extract zip file to.
     * @param file
     *            Zip file.
     */
    public static void extractZipFile(final File dir, final File file) {
        try {
            FileInputStream fis = new FileInputStream(file);
            extractZipFile(dir, new BufferedInputStream(fis));
        } catch (FileNotFoundException e) {
            Util.log(e);
        }
    }

    /**
     * Extract zip file.
     *
     * @param dir
     *            Directory to extract zip file to.
     * @param bis
     *            Input stream that has zip file contents.
     */
    public static void extractZipFile(final File dir,
            final BufferedInputStream bis) {
        try {

            mkdirs(dir);

            if (!dir.isDirectory()) {
                return;
            }

            ZipInputStream zis = new ZipInputStream(bis);
            ZipEntry ze = zis.getNextEntry();

            while (ze != null) {

                if (ze.isDirectory()) {
                    ze = zis.getNextEntry();
                    continue;
                }

                File f = new File(dir + "/" + ze.getName());

                mkdirs(f.getParentFile());

                copyOpenStream(new BufferedInputStream(zis),
                        new BufferedOutputStream(new FileOutputStream(f)));

                zis.closeEntry();

                ze = zis.getNextEntry();
            }

            zis.close();

        } catch (Exception e) {
            Util.log(e);
        }
    }

    /**
     * Copy open stream.
     *
     * @param bis
     *            Buffered input stream.
     * @param bos
     *            Buffered output stream.
     */
    public static void copyOpenStream(final BufferedInputStream bis,
            final BufferedOutputStream bos) {
        boolean done = false;
        try {
            synchronized (bis) {
                synchronized (bos) {
                    byte[] buff = new byte[Numbers.Integers.BUFFER_SIZE
                            .getValue()];
                    while (!done) {
                        int c = bis.read(buff);
                        if (c == -1) {
                            done = true;
                        } else {
                            bos.write(buff, 0, c);
                        }
                    }
                }
            }
            bos.flush();
            bos.close();
        } catch (IOException ex) {
            Util.log(ex);
        }
    }

    /**
     * Get image from file.
     *
     * @param f
     *            File.
     * @return Image.
     */
    public static Image getImageFromFile(final File f) {
        try {
            Image rtn = null;
            if (f == null) {
                Util.log(Util.x("File is null"));
                return null;
            }
            if (!f.exists()) {
                Util.log(RezIO.getCanonicalPath(f) + " "
                        + Util.x("does not exist") + ".");
                return null;
            }
            if (!f.canRead()) {
                Util.dspmsg(Util.x("Check permissions") + ".");
                Util.log(RezIO.getCanonicalPath(f));
                return null;
            }
            if (!Util.checkMemory(f.length())) {
                Util.dspmsg(Util.x("Out of memory, please check log") + ".");
            }
            Util.dspmsg(Util.x("loading") + " " + f.getName() + " ...");
            rtn = ImageIO.read(f);
            Util.dspmsg(f.getName() + " " + Util.x("loaded") + ".");
            return rtn;
        } catch (IOException e) {
            Util.dspmsg(e.getMessage());
            Util.log(e);
            return null;
        }
    }

    /**
     * Combine paths.
     *
     * @param path0
     *            Path 0.
     * @param path1
     *            Path 1.
     * @return path0 + System.getProperty("file.separator") + path1.
     */
    public static String combinePaths(final File path0, final String path1) {
        return combinePaths(RezIO.getCanonicalPath(path0), path1);
    }

    /**
     * Combine paths.
     *
     * @param path0
     *            Path 0.
     * @param path1
     *            Path 1.
     * @return path0 + System.getProperty("file.separator") + path1.
     */
    public static String combinePaths(final String path0, final String path1) {
        StringWriter sw = new StringWriter();
        sw.append(path0);
        sw.append(Strings.FILE_SEP);
        sw.append(path1);
        return sw.toString();
    }

    /**
     * Validate that a file exists, and it is not a directory.
     *
     * @param file
     *            File to check.
     * @return boolean true if valid file found
     */
    public static boolean validateFile(final File file) {
        return ((validateExistence(file)) && (file.isFile()));
    }

    /**
     * Validate that a file exists, and that it is a directory.
     *
     * @param dir
     *            Directory to check.
     * @return boolean true if valid directory found
     */
    public static boolean validateDirectory(final File dir) {
        if (validateExistence(dir)) {
            if (dir.isDirectory()) {
                return true;
            }
            Util.dspmsg(Util.x("File") + " " + RezIO.getCanonicalPath(dir)
                    + " " + Util.x("is not a directory"));
        } else {
            if (dir == null) {
                return false;
            }
            if (!dir.mkdirs()) {
                Alert.yellow("Failed to create" + ": "
                        + RezIO.getCanonicalPath(dir));
            }
        }
        return ((validateExistence(dir)) && (dir.isDirectory()));
    }

    /**
     * Validate existence of a file.
     *
     * @param file
     *            File to check.
     * @return True if file exists.
     */
    private static boolean validateExistence(final File file) {
        if (file == null) {
            Util.dspmsg(Util.x("file is null"));
            return false;
        }
        if (file.exists()) {
            return true;
        }
        Util.dspmsg(Util.x("file") + " " + RezIO.getCanonicalPath(file) + " "
                + Util.x("does not exist"));
        return false;
    }

    /**
     * Load text from resource.
     *
     * @param resourcePath
     *            Resource path.
     * @param textFilter
     *            Text filter, optional (can be null).
     * @param clazz
     *            Class in same jar as resource.
     * @return Contents of resource as a String.
     * @throws IOException
     *             IOException.
     */
    public static String loadTextFromResource(final String resourcePath,
            final TextFilter textFilter, final Class<?> clazz)
            throws IOException {
        StringWriter sw = new StringWriter();
        for (String s : RezIO.loadLinesFromResource(resourcePath, clazz)) {
            if (textFilter == null) {
                sw.append(s);
            } else {
                sw.append(textFilter.filter(s));
            }
        }
        return sw.toString();
    }

    /**
     * @param clazz
     *            Class in same jar as resource.
     * @param resourcePath
     *            Resource path.
     * @return All lines from resourcePath except empty or blank.
     * @throws IOException
     *             IOException.
     */
    public static List<String> loadLinesFromResourceTrim(final Class<?> clazz,
            final String resourcePath) throws IOException {

        List<String> rtn = new ArrayList<String>();
        for (String s : RezIO.loadLinesFromTextResource(clazz, resourcePath)) {
            if (Strings.isPopulated(s.trim())) {
                rtn.add(s);
            }
        }
        return rtn;
    }

    /**
     *
     * @param resourcePath
     *            Resource path.
     *
     * @param clazz
     *            Class in same jar as resource.
     * @return All lines from resourcePath except empty or blank.
     */
    public static List<String> loadLinesFromResourceTrim(

    final String resourcePath, final Class<?> clazz) {

        List<String> rtn = new ArrayList<String>();
        for (String s : loadLinesFromResource(resourcePath, clazz)) {
            if (Strings.isPopulated(s.trim())) {
                rtn.add(s);
            }
        }
        return rtn;
    }

    /**
     * Load lines from resource.
     *
     * @param resourcePath
     *            Resource path to a text file.
     * @param clazz
     *            Class in same jar as resource.
     * @return List of lines.
     */
    public static List<String> loadLinesFromResource(

    final String resourcePath, final Class<?> clazz) {
        List<String> rtn = new ArrayList<String>();
        try {
            BufferedReader br = new BufferedReader(new StringReader(
                    loadTextFromResource(resourcePath, null, clazz)));
            String s = br.readLine();
            while (s != null) {
                rtn.add(s);
                s = br.readLine();
            }
        } catch (IOException e) {
            // Just return an empty List if the resource is not there.
            return rtn;
        }
        return rtn;
    }

    /**
     * Create a file object with a given directory and simple name.
     *
     * @param directory
     *            Directory.
     * @param name
     *            File name.
     * @return File object.
     */
    public static File createFileFromDirectory(final File directory,
            final String name) {
        return new File(RezIO.getCanonicalPath(directory) + Strings.FILE_SEP
                + name);
    }

    /**
     * Prevent instantiation.
     */
    private UtilIO() {
    }

    /**
     * Pipe | map separator is the default, to avoid conflicts with Base64
     * encoded keys.
     */
    public static final String MAP_SEP = "|";

    /**
     * Save map to a file.
     *
     * @param map
     *            Map.
     * @param file
     *            File.
     */
    public static void saveMap(final Map<String, String> map, final File file) {
        LockedWriter lw = new LockedWriter(file);
        for (Entry<String, String> e : map.entrySet()) {
            StringWriter sw = new StringWriter();
            sw.append(e.getKey());
            sw.append(MAP_SEP);
            sw.append(Base64.encode(e.getValue()));
            lw.writeln(sw.toString());
        }
        lw.close();
    }

    /**
     * Load map from a file.
     *
     * @param file
     *            File.
     * @return Map.
     */
    public static Map<String, String> loadMap(final File file) {
        final Map<String, String> map = Collections
                .synchronizedMap(new TreeMap<String, String>());
        new FileProcessor(file, new Listener() {
            @Override
            public void process(final FileProcessor pr, final String[] s) {
                if (Strings.isPopulated(s[0])) {
                    String[] sp = split(s[0]);
                    if (Strings.isPopulated(sp[0])
                            && Strings.isPopulated(sp[1])) {
                        byte[] enc = Base64.decode(sp[1]);
                        if (enc != null) {
                            map.put(sp[0], new String(enc));
                        }
                    }
                }
            }

            @Override
            public void finish(final FileProcessor processor) {
            }
        }).exec();
        return map;
    }

    /**
     * @param s
     *            String to split
     * @return String array: s[0] = key, s[1] = value.
     */
    public static String[] split(final String s) {
        return Strings.split(s, MAP_SEP);
    }

    /**
     * Copy directory.
     *
     * @param from
     *            From directory.
     * @param to
     *            To directory.
     * @return true iff Completely successful
     */
    public static boolean copyDirectory(final File from, final File to) {
        return copyDirectory(from, to, false);
    }

    /**
     * Copy directory, but only new files. Existing files will not be
     * overwritten, even if they are older.
     *
     * @param from
     *            From directory.
     * @param to
     *            To directory.
     * @return true iff Completely successful
     */
    public static boolean copyDirectoryNewOnly(final File from, final File to) {
        return copyDirectory(from, to, true);
    }

    /**
     * Copy directory.
     *
     * @param from
     *            From directory.
     * @param to
     *            To directory.
     * @param newOnly
     *            If true, only copy files over if they don't already exist. If
     *            false, copy everything over, overwriting anything that is
     *            already there.
     * @return true iff Completely successful
     */
    private static boolean copyDirectory(final File from, final File to,
            final boolean newOnly) {
        if (from == null || to == null) {
            return false;
        }
        if (!from.exists()) {
            Alert.yellow(RezIO.getCanonicalPath(from) + " "
                    + Util.x("not found") + ".");
            return false;
        }
        if (!from.isDirectory()) {
            return false;
        }
        boolean rtn = true;
        for (File f : from.listFiles()) {
            File n = new File(combinePaths(RezIO.getCanonicalPath(to),
                    f.getName()));
            if (f.isDirectory()) {
                if (!copyDirectory(f, n)) {
                    rtn = false;
                }
            } else {
                if (newOnly && n.exists()) {
                    continue;
                }
                if (!copyFile(f, n)) {
                    rtn = false;
                }
            }
        }
        return rtn;
    }

    /**
     * Clean and copy a directory.
     *
     * @param from
     *            From directory.
     * @param to
     *            To directory, everyting will be erased in here first.
     * @return true iff Completely successful
     */
    public static boolean copyDirectoryClean(final File from, final File to) {
        clean(to);
        return copyDirectory(from, to);
    }

    /**
     * Clean out a directory, but skip hidden files.
     *
     * @param dir
     *            Directory to clean.
     */
    public static void clean(final File dir) {
        if (dir == null) {
            return;
        }
        if (dir.isFile()) {
            return;
        }
        File[] tl = dir.listFiles();
        if (tl != null) {
            for (File d : tl) {
                if (d.getName().indexOf(".") == 0) {
                    continue;
                }
                if (d.isDirectory()) {
                    clean(d);
                }
                UtilIO.delete(d);
            }
        }
    }

    /**

     */
    /**
     * Home directory.
     */
    private static String home;

    /**
     * @return Home directory.
     */
    public static String getHome() {
        if (home == null) {
            home = System.getenv().get("HOMEDRIVE");
            if (home == null) {
                home = System.getProperty("user.home");
                if (home == null) {
                    home = "";
                }
                if (home.indexOf(":") == 1) {
                    home = home.substring(0, 2);
                }
            }
        }
        return home;
    }

    /**
     * Save lines to a file.
     *
     * @param lines
     *            List of lines.
     * @param file
     *            File.
     */
    public static void saveLines(final List<String> lines, final File file) {
        if (lines == null || file == null) {
            return;
        }
        try {
            FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw);
            for (String s : lines) {
                bw.write(s);
                bw.write(Strings.getEol());
            }
            bw.flush();
            bw.close();
            fw.close();
        } catch (IOException e) {
            Util.log(e);
        }
    }

    /**
     *
     * @param f
     *            File.
     * @return Extension of file.
     */
    public static String getExtension(final File f) {
        return Strings.getExtension(f.getName());
    }
}
